网络时钟对齐解决方案

由于网络延迟等问题,Client 时间与 DS 时间可能不同。

对于 Client 需要有一个接近正确的 GetServerWorldTime 来获取当前 DS 的时间戳,尽可能保证在 DS 与各个不同的 Client 中,同一时刻该值唯一。

GameInstance

通过 GameInstance 来存储一些时间数据记录,最后保证所有读取数据从这里访问:

1
2
3
int64 StartTicks = 0;
float WorldSeconds = 0;
float ServerWorldSeconds = 0;

首先在 UGameInstance::Init() 时,记录 StartTicks = FDateTime::Now().GetTicks()

后续在 GeServerWorldTimeSeconds 时,对 ServerWorldSecondsWorldSeconds 进行更新;

ServerTimeSynchronizer

Register

ClientPlayerController 成功 Received 时,进行对时校验的注册,每隔 SyncServerTime_TimeInterval = 5.0f 进行一次对时;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
void APlayerController::ReceivedPlayer()
{
// ...
if (IsLocalController())
{
ClientStartSyncTime();
}
}

void APlayerController::ClientStartSyncTime()
{
if (GetNetMode() != NM_Client) return;

GetWorld()->GetTimerManager().SetTimer(SyncServerTime_TimeHandle, [WeakSelfPtr = TWeakObjectPtr<APlayerController>(this)]()
{
if (!WeakSelfPtr.IsValid() || WeakSelfPtr->GetNetMode() != NM_Client) return;

if (!WeakSelfPtr->ServerTimeSynchronizer.IsTimeSynced())
{
WeakSelfPtr->C2S_ReqReportTime_Reliable(FDateTime::Now().GetTicks());
}
else
{
WeakSelfPtr->C2S_ReqReportTime_Unreliable(FDateTime::Now().GetTicks());
}
}, SyncServerTime_TimeInterval, true);
}

Req & Res

通过 Client 定时发起对时请求 C2S_ReqReportTimeDS 接收到请求后进行回复 S2C_ResReportTime(如果从未进行过对时,则需要 Reliable);

Client 收到 Res 之后,可以根据发包、收包的时间差,不断校准 ServerTime

1
2
3
4
5
6
7
8
9
10
UFUNCTION(Reliable, Server)
void C2S_ReqReportTime_Reliable(int64 ClientTime);
UFUNCTION(Unreliable, Server)
void C2S_ReqReportTime_Unreliable(int64 ClientTime);

void OnReceivedServerTime(int64 ClientTime, int64 ServerTime);
UFUNCTION(Reliable, Client)
void S2C_ResReportTime_Reliable(int64 ClientTime, int64 ServerTime);
UFUNCTION(Unreliable, Client)
void S2C_ResReportTime_Unreliable(int64 ClientTime, int64 ServerTime);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
void APlayerController::C2S_ReqReportTime_Reliable_Implementation(int64 ClientTime)
{
UGameInstance* GameInstance = GetGameInstance();
if (GameInstance == nullptr) return;

if (GameInstance->IsDedicatedServerInstance() && GameInstance->StartTicks > 0)
{
S2C_ResReportTime_Reliable(ClientTime, FDateTime::Now().GetTicks() - GameInstance->StartTicks);
}
}

void APlayerController::C2S_ReqReportTime_Unreliable_Implementation(int64 ClientTime)
{
UGameInstance* GameInstance = GetGameInstance();
if (GameInstance == nullptr) return;

if (GameInstance->IsDedicatedServerInstance() && GameInstance->StartTicks > 0)
{
S2C_ResReportTime_Unreliable(ClientTime, FDateTime::Now().GetTicks() - GameInstance->StartTicks);
}
}

// --------------------

void APlayerController::OnReceivedServerTime(int64 ClientTime, int64 ServerTime)
{
int64 ClientNow = FDateTime::Now().GetTicks();
int64 RTT = ClientNow - ClientTime;

bool bUpdated = ServerTimeSynchronizer.UpdateServerTime(ServerTime, ClientNow, RTT);
int64 EstimatedServerTime = ServerTimeSynchronizer.CurrentServerTime(ClientNow);

if (!ServerTimeSynchronizer.IsTimeSynced())
{
C2S_ReqReportTime_Reliable(FDateTime::Now().GetTicks());
}
}

void APlayerController::S2C_ResReportTime_Reliable_Implementation(int64 ClientTime, int64 ServerTime)
{
OnReceivedServerTime(ClientTime, ServerTime);
}

void APlayerController::S2C_ResReportTime_Unreliable_Implementation(int64 ClientTime, int64 ServerTime)
{
OnReceivedServerTime(ClientTime, ServerTime);
}

Calculate

通过 FServerTimeSynchronizer 记录时间并辅助对时;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class ENGINE_API FServerTimeSynchronizer
{
public:
FServerTimeSynchronizer() = default;

public:
int64 CurrentServerTime(int64 TimeNow = 0) const;
bool UpdateServerTime(int64 ServerTime, int64 CurrentTime, int64 LatestRTT);
bool IsTimeSynced() const { return bTimeSynced; } // 进行过时钟对齐

private:
int32 CalculateApproximatingDeltaTime(int64 TimeDelta) const;

private:
static constexpr int MAX_VALID_RTT = 500; // 最大能容忍的RTT时间, 超过该时间不作为有效校准数值 (单位 MS)
static constexpr float APPROXIMATING_RATE = 0.33; // 逼近率 (每个单位时间客户端需要逼近服务器的值)

private:
int64 ClientTime = 0;
int64 EstimatedServerTime = 0;
int LatestRTT = INT_MAX;
float DeltaTimeClientToServer = 0.0;
bool bTimeSynced = false;
};
flowchart LR
Client_Req--->|RTT|DS
DS--->|RTT|Client_Res

每次收到包进行 UpdateServerTime,根据时间差计算出 RTT (收包发包时间差 / 2) 加上当时准确的 ServerTimeRPC 带下来的),可以计算出此时客户端对应的 EstimatedServerTime(此时预估的 ServerTime);同时记录下这次的 ClientTime

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
bool FServerTimeSynchronizer::UpdateServerTime(int64 ServerTime, int64 CurrentTime, int64 RTT)
{
// 丢弃过大的 RTT 时间
if (RTT > MAX_VALID_RTT * ETimespan::TicksPerMillisecond)
return false;

// 仅用更小的 RTT 进行当前 EstimatedServerTime 的更新
if (RTT > LatestRTT)
return false;

int64 LastEstimatedServerTime = CurrentServerTime(CurrentTime);
ClientTime = CurrentTime;
EstimatedServerTime = ServerTime + RTT / 2;
LatestRTT = RTT;

// 首次更新 ServerTime 或 APPROXIMATING_RATE <= 0, 无需时钟逼近
if ( !bTimeSynced || APPROXIMATING_RATE <= 0)
{
DeltaTimeClientToServer = 0.0;
}
else
{
// 之后的计算逼近到以 EstimatedServerTime 为基准的时间轴
DeltaTimeClientToServer = (float)(LastEstimatedServerTime - EstimatedServerTime);
}

return bTimeSynced = true;
}

这样就可以在后续任何一次查询时,根据查询时的 ClientTime ,与本次对时的预估 ServerTimeClientTime 计算出期望的 ServerTime

1
2
3
4
5
6
int64 FServerTimeSynchronizer::CurrentServerTime(int64 TimeNow) const
{
if (!bTimeSynced) return 0;
int64 TimeDelta = FMath::Max( TimeNow - ClientTime, 0ll );
return EstimatedServerTime + TimeDelta + CalculateApproximatingDeltaTime(TimeDelta);
}

同时,这里引入一个 APPROXIMATING_RATE,进行一定的时间预测逼近,在 DeltaTime 足够小的时候,根据 DeltaTimeClientToServer(客户端与服务器的预测时间差值)进行一定的时间外推 / 内收,让结果尽可能准确。这里 DeltaTimeClientToServer < 0 说明客户端时间比服务器慢,则需要一定的加快。

1
2
3
4
5
6
7
8
9
10
11
int32 FServerTimeSynchronizer::CalculateApproximatingDeltaTime(int64 TimeDelta) const
{
if (APPROXIMATING_RATE <= 0)
return 0;

float fEls = float(TimeDelta) * APPROXIMATING_RATE;
if (fEls >= FMath::Abs(DeltaTimeClientToServer))
return 0;

return DeltaTimeClientToServer < 0 ? ceil(DeltaTimeClientToServer + fEls) : ceil(DeltaTimeClientToServer - fEls);
}

GetServerTimeTicks

最后在 PlayerController 暴露 GetServerTimeTicks 给外部访问:

1
2
3
4
int64 APlayerController::GetServerTimeTicks()
{
return ServerTimeSynchronizer.CurrentServerTime(FDateTime::Now().GetTicks());
}

GetServerWorldTimeSeconds

AGameStateBase::GetServerWorldTimeSeconds 进行重载,最后统一通过 WorldGameState 进行时间访问;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
double AGameStateBase::GetServerWorldTimeSeconds() const
{
UWorld* World = GetWorld();
if (World == nullptr) return 0.;

if (UGameInstance* GameInstance = GetGameInstance())
{
float NowSeconds = GetWorld()->TimeSeconds;

// Update ServerWorldSeconds
if (NowSeconds != GameInstance->WorldSeconds)
{
GameInstance->WorldSeconds = NowSeconds;

int64 NowServerTicks = 0;
if (GameInstance->IsDedicatedServerInstance() || HasAuthority())
{
NowServerTicks = FDateTime::Now().GetTicks() - GameInstance->StartTicks;
}
else if (APlayerController* PC = GetGameInstance()->GetFirstLocalPlayerController(GetWorld()))
{
NowServerTicks = PC->GetServerTimeTicks();
}

GameInstance->ServerWorldSeconds = NowServerTicks / ETimespan::TicksPerMillisecond / 1000.f;
}

return GameInstance->ServerWorldSeconds;
}

return World->GetTimeSeconds() + ServerWorldTimeSecondsDelta;
}